package edu.ucla.cs.rpc.multicast.util.token;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ConnectException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketTimeoutException;
import java.util.concurrent.TimeUnit;

/**
 * Network version of a TokenManager. An instance of this class is used as a
 * daemon process in its own thread to receive Tokens from other processes
 * accross a network. The instance is also used to send Tokens to other
 * processes. An implemenation of the a token ring.
 * 
 * @author Philip Russell Chase Covello
 * 
 */
public class NetworkTokenManager extends TokenManager {

	private static int counter = 0;

	private ServerSocket listener = null;

	private SocketAddress next = null;

	private NetworkTokenManager() {
	}

	public int id;

	/**
	 * Primary constructor. Creates a new NetworkTokenManager bound to the given
	 * address. NOTE: this class sets a timeout value of 1000 ms for listening
	 * for the token.
	 * 
	 * @param address
	 *            The address to bind this process to.
	 * @throws IOException
	 *             If a lower-level network operation failed, such as creating
	 *             or binding the socket.
	 */
	public NetworkTokenManager(SocketAddress address) throws IOException {
		super();
		this.listener = new ServerSocket();
		this.listener.bind(address);
		this.listener.setSoTimeout(1000);

		this.id = counter;
		counter++;
	}

	/**
	 * Secoundary constructor, for testing purposes.
	 * 
	 * @param address
	 *            The address that this token manager will listen for tokens on.
	 * @param id
	 *            Optional id, for testing purposes.
	 * @throws IOException
	 */
	public NetworkTokenManager(SocketAddress address, int id)
			throws IOException {
		super();
		this.listener = new ServerSocket();
		this.listener.bind(address);
		this.listener.setSoTimeout(1000);
		this.id = id;
	}

	/**
	 * 
	 * @return The address that this TokenManager is listening for tokens on.
	 */
	public SocketAddress getAddress() {
		return this.listener.getLocalSocketAddress();
	}

	/**
	 * 
	 * @return The address of the next TokenManager in the ring.
	 */
	public SocketAddress getNextAddress() {
		return this.next;
	}

	/**
	 * Method to set the next TokenManager in the ring, using addresses.
	 * 
	 * @param next
	 *            This TokenManager's next neighbor.
	 */
	public void setNextAddress(SocketAddress next) {
		this.next = next;
	}

	/**
	 * @return A descriptive string of this object.
	 */
	public String toString() {
		StringBuffer buffer = new StringBuffer();
		buffer.append("Local address = ");
		buffer.append(this.listener.getLocalSocketAddress().toString());
		buffer.append("\n");

		buffer.append("Next address = ");

		if (this.next == null) {
			buffer.append("null.");
		} else {
			buffer.append(this.next.toString());
		}
		buffer.append("\n");

		return buffer.toString();
	}

	/**
	 * Sends the token to the next TokenManager in the ring.
	 * 
	 * @throws TokenNotHeldException
	 *             Thrown if this object does not hold the token.
	 */
	public synchronized void sendToken() throws TokenNotHeldException,
			IOException {
		if (!this.hasToken()) {
			throw new TokenNotHeldException();
		}

		Socket socket = new Socket();
		socket.connect(this.next);
		ObjectOutputStream output = new ObjectOutputStream(socket
				.getOutputStream());

		output.writeObject(this.token);

		output.close();
		socket.close();

	}

	public void run() {
		System.out.println("NetworkTokenHolder running");
		running = true;

		while (running) {
			try {

				this.getLock().lock();

				while (this.hasToken() != true) {

					Socket request = listener.accept();
					ObjectInputStream data = new ObjectInputStream(request
							.getInputStream());
					final Token token = (Token) data.readObject();
					data.close();
					request.close();

					this.setToken(token);
					System.out.println("Node " + id + " got the token!");
					this.getCondition().signal();
				}

				System.out.println("Node " + id + " has the token!");

				this.tokenCondition.await(TokenManager.TOKENHOLD_TIMEOUT,
						TimeUnit.MILLISECONDS);

				if (this.next != null) {
					this.sendToken();
					this.clearToken();
					System.out.println("Node " + id + " sent the token!");
				} else {
					System.out.println("Node " + id
							+ " is not connected, terminated.");
				}

			} catch (SocketTimeoutException e) {
				// Socket timeout, just re-loop
			} catch (ConnectException e) {
				// Can't connect to the next sender.
			} catch (IOException e) {
				// e.printStackTrace();
			} catch (ClassNotFoundException e) {
				// e.printStackTrace();
			} catch (InterruptedException e) {
				// e.printStackTrace();
			}
			this.getLock().unlock();
		}

		try {
			this.listener.close();
		} catch (IOException e) {
			e.printStackTrace();
		}

		System.out.println("Node #" + id + " completed.");
		return;
	}

}
